/**
 * project3D Engine
 * @author John Sword
 * @version 2 - AS3
 * 
 * using Left-handed Coordinate System 
 *    
 *    Z
 *   /
 *  /__ X
 * |
 * |
 * Y
 * based loosely on work by BrandonWilliams
 * and Carlos Ulloa Matesanz (Papervision)
 * 
 * Copyright 2007 (c) John Sword
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

package engine
{

	import engine.camera.Camera3D;
	import engine.geom.Face;
	import engine.geom.Vector;
	import engine.geom.convexHull2D;
	import engine.objects.Object3D;
	
	import flash.display.Graphics;
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	import flash.geom.Rectangle;
	import flash.utils.Timer;
	
	public class Engine3D 
	{
		
		/* public */
		public var zClipping:Number = 0;
		public var renderSpeed:Number = 45;
		public var minHullSize:int = 2000;
		public var maxHullSize:int = 600000;
		public var occlusion:Boolean = false;
		/* private */
		private var visiblePolys:Number = 0;
		private var SCREEN_WIDTH:Number;
		private var SCREEN_HEIGHT:Number;
		private var WorldObjects:Array;
		private var WorldBlockers:Array;
		private var WorldMC:Sprite;
		private var WorldCam:Camera3D;
		private var totalObjects:int = 0;
		private var renderTimer:Timer;
		private var mouseOverFlag:Boolean = false;
		/* static */
		public static const QUALITY_LOW:String = "LOW";
		public static const QUALITY_AUTOLOW:String = "AUTOLOW";
		public static const QUALITY_MEDIUM:String = "MEDIUM";
		public static const QUALITY_HIGH:String = "HIGH";
		public static const QUALITY_AUTOHIGH:String = "AUTOHIGH";
		
		//private static var sin:Function = Math.sin;
		//private static var cos:Function = Math.cos;
		//private const sin:Function = Math.sin;
		//private const cos:Function = Math.cos;
		private static var mouseOverObj:Object3D;
		private static var mouseOutObj:Object3D;
		private static var g:Graphics;
		private static const toDEGREES:Number = 180 / Math.PI;
        private static const toRADIANS:Number = Math.PI / 180;
		
		private static const polyBuffer:Array = new Array();
		
		
		/**
		* Constructor
		* @param x the X position of the 3D screen
		* @param y the Y position of the 3D screen
		* @param w the Width of the 3D screen
		* @param h the Height of the 3D screen
		*/
		public function Engine3D ( screen:Sprite, x:int=0, y:int=0, w:int=640, h:int=480 )
		{
			// set 3d engine screen size
           	WorldMC = new Sprite();
           	WorldMC.graphics.moveTo(x,y);
           	WorldMC.graphics.lineTo(w,y);
           	WorldMC.graphics.lineTo(w,h);
           	WorldMC.graphics.lineTo(x,h);
			WorldMC.cacheAsBitmap = true;
			SCREEN_WIDTH = WorldMC.width;
			SCREEN_HEIGHT = WorldMC.height;
			WorldObjects = new Array();
			WorldBlockers = new Array();
			g = WorldMC.graphics;
			//polyBuffer = new Array();
			//polyBuffer;
			// create a default camera
			setCamera( new Camera3D ( WorldMC ) );
			// clip screen
			WorldMC.scrollRect = new Rectangle(x,y,SCREEN_WIDTH,SCREEN_HEIGHT);
			// ini render timer with default speed
			renderTimer = new Timer( renderSpeed );
			// listener for object mouse events
			WorldMC.addEventListener (MouseEvent.CLICK, MouseEventCLICK, false, 0, true);
			WorldMC.addEventListener (MouseEvent.MOUSE_DOWN, MouseEventMouseDown, false, 0, true);
			WorldMC.addEventListener (MouseEvent.MOUSE_UP, MouseEventMouseUp, false, 0, true);
			// show 3d world
			screen.addChild( WorldMC );
			trace("Engine 3D init");
		}
		
		public function getWorld () : Sprite
		{
			return WorldMC;
		}
		
		public function applyFilters ( filters:Array ) : void
		{
			WorldMC.filters = filters;
		}
		
		public function setQuality ( setting:String ) : void
		{
			WorldMC.stage.quality = setting;
		}
		
		public function setCamera ( c:Camera3D ) : void
		{
			WorldCam = c;
		}
	
		public function getCamera () : Camera3D
		{
			return WorldCam;
		}
		
		public function getWidth () : Number
		{
			return SCREEN_WIDTH;
		}
		
		public function getHeight () : Number
		{
			return SCREEN_HEIGHT;
		}
		
		public function getVisiblePolys () : Number
		{
			return visiblePolys;
		}
		
		public function addObject ( o:Object3D, n:String = "obj" ) : void
		{
			// TODO: check for duplicate names?
			WorldObjects.push (o);
			totalObjects = WorldObjects.length;
			if( n == "obj" ) n = n + totalObjects.toString();
			o.objID = totalObjects;
			o.objName = n;
		}
		
		public function render() : void
		{
			if ( !renderTimer.running )
			{
				renderTimer = new Timer( renderSpeed );
				renderTimer.addEventListener(TimerEvent.TIMER, _onEnterFrame);
				renderTimer.start();
			}
		}
		
		public function stopRender() : void
		{
			renderTimer.stop();
		}
		
		private function _onEnterFrame( event:TimerEvent ) : void
		{
			WorldMC.visible = false;
			WorldMC.buttonMode = false;
			WorldMC.useHandCursor = false;
			/*
			if ( WorldMC.numChildren > 0 ) 
			{
				var bmp:Bitmap = WorldMC.getChildAt( 0 ) as Bitmap;
				bmp.bitmapData.fillRect( new Rectangle ( 0,0,SCREEN_WIDTH,SCREEN_HEIGHT ), 0x00 );
			}
			*/
			mouseOverObj = null;
			// clear faces from buffer array
			//polyBuffer = new Array();
			polyBuffer.splice(0);
			// update camera rotation if need be
			if ( WorldCam.renderMe ) WorldCam.update_rotation_matrix ();
			// clear screen
			g.clear();
			// render world objects
			var w:Array = WorldObjects;
			var i:int = totalObjects;
			if ( occlusion ) {
				WorldBlockers.splice(0);
				w.sortOn ( 'screenZ', Array.DESCENDING | Array.NUMERIC );
			}
			var o:Object3D;
			while (o = w[--i])	{
				// invisible objects dont project
				if ( o.isVisible )
				{
					worldToCam ( o ); // transform object
					// dont render if object faces are'nt visible
					if ( o.facesVisible ) {
						if ( occlusion ) {
							//trace( o.objName + " " + o.getObjectArea() );
							var area:int = o.getObjectArea()
							if ( area > minHullSize && area < maxHullSize )
								buildHull( o );
							if ( !testHulls( o.screenZ,o.OBB ) ) continue;
							//trace( o.objName + " " + o.screenZ );
						}
						testFaces( o );
					}
				}
				
			}
			// render object's faces (polys)
			renderPolys ();
			// show world
			WorldMC.cacheAsBitmap = true;
			WorldMC.visible = true;
			WorldCam.renderMe = false;
		}
		
		private function worldToCam ( obj:Object3D ) : void 
		{
			var o:Object3D = obj; // local object reference
			// transform object relative to world cam
			o.project ( WorldCam, occlusion );
		}
		
		private function buildHull ( obj:Object3D ) : void
		{
			var o:Object3D = obj; // local object reference
			var hull:convexHull2D = new convexHull2D();
			var n:int = hull.buildApproximate( o.tVertices );
			if ( n <= 3 ) return;
			hull.screenZ = o.screenZ;
			WorldBlockers.push( hull );
			o.hull = hull;
			//hull.render( g );
			/*
			var bb:Object = o.OBB;
			g.lineStyle( 5, 0xFF0000, 1);
			g.moveTo( bb.min.x,bb.min.y );
			g.lineTo( bb.max.x,bb.min.y );
			g.lineTo( bb.max.x,bb.max.y );
			g.lineTo( bb.min.x,bb.max.y );
			g.lineTo( bb.min.x,bb.min.y );
			g.lineStyle();
			*/
			//if ( hull.contains( WorldMC.mouseX, WorldMC.mouseY ) )
			//	trace( "contact at x: " + WorldMC.mouseX + " y: " + WorldMC.mouseY );
			//else
			//	trace( "x: " + WorldMC.mouseX + " y: " + WorldMC.mouseY );
           
		}
		
		private function testHulls ( screenZ:Number, obb:Object ) : Boolean
		{ 
			var wb:Array = WorldBlockers;
			var hlen:int = wb.length;
			if ( hlen == 1 ) return true;
			var i:int = 0;
			var tmph:convexHull2D;
			var obbMaxX:int = obb.max.x;
			var obbMinX:int = obb.min.x;
			var obbMaxY:int = obb.max.y;
			var obbMinY:int = obb.min.y;
			for each ( tmph in wb )
			{
				if ( tmph.screenZ >= screenZ  ) continue;
				if ( tmph.contains( obbMaxX,obbMinY ) )
					if ( tmph.contains( obbMaxX,obbMaxY ) )
						if ( tmph.contains( obbMinX,obbMaxY ) )
							if ( tmph.contains( obbMinX,obbMinY ) )
								return false;
			}
			return true;
		}
		
		private function testFaces ( obj:Object3D ) : void
		{
			var o:Object3D = obj; // local object reference
			var faces:Array = o.faces;
			var f:Face;
			var i:int = faces.length;
			var pb:Array = polyBuffer;
			// push available faces to poly buffer
			while ( f = faces[--i] )
			{
				if ( f.setVisible (WorldMC,-zClipping,o.flipNormals,o.cull) ) pb.push (f);
			}
		}
		
		private function renderPolys () : void
		{
			// local variables for speeding up
			var vp:int = 0;
			var wmc:Sprite = WorldMC;
			// get poly list
			var pb:Array = polyBuffer;
			// tmp object for mouse events
			var o:Object3D = null;
			//g.clear();
			//pb.sortOn ( 'Z', Array.DESCENDING | Array.NUMERIC );
			// sort visible polys
			pb.sortOn ( 'Z', Array.NUMERIC );
			var i:int = pb.length;
			while ( --i > -1 )
			{	
				// renders face and retrieves current object under the mouse if any
				o = pb[i].render ( wmc );
				if ( o ) mouseOverObj = o;
				++vp;
			}
			// deal with mouse events on object closer to the camera
			// only deals with one object at a time
			if ( mouseOverObj ) 
			{
				if ( mouseOverObj !== mouseOutObj && mouseOutObj != null )
				{
					mouseOutObj.broadcastMOUSEOUT();
					mouseOutObj = null;
				}
				if ( !mouseOutObj )
				{
					mouseOverObj.broadcastMOUSEOVER();
					mouseOverFlag = true;
					mouseOutObj = mouseOverObj;
				}
				wmc.buttonMode = wmc.useHandCursor = true;
			} else {
				if ( mouseOutObj )
				{
					mouseOutObj.broadcastMOUSEOUT();
					mouseOutObj = null;
				}
			}
			visiblePolys = vp;
		}
		
		private function PolygonArea (v0:Vector,v1:Vector,v2:Vector) : Number 
	    {
	    	return .5 * (v0.x*v1.y-v1.x*v0.y) + (v1.x*v2.y-v2.x*v1.y) + (v2.x*v0.y-v0.x*v2.y);
		}
		
		private function contains(x:Number, y:Number, f:Face):Boolean
        {   
            var v0:Vector = f.v1.screen;
            var v1:Vector = f.v2.screen;
            var v2:Vector = f.v3.screen;
            
            if (v0.x*(y - v1.y) + v1.x*(v0.y - y) + x*(v1.y - v0.y) < -0.001)
                return false;

            if (v0.x*(v2.y - y) + x*(v0.y - v2.y) + v2.x*(y - v0.y) < -0.001)
                return false;

            if (x*(v2.y - v1.y) + v1.x*(y - v2.y) + v2.x*(v1.y - y) < -0.001)
                return false;

            return true;
        }
		
		// mouse events
		private function MouseEventCLICK ( e:MouseEvent ) : void
		{
			if ( mouseOverObj ) mouseOverObj.broadcastCLICK ();
		}
		
		private function MouseEventMouseDown ( e:MouseEvent ) : void
		{
			if ( mouseOverObj ) mouseOverObj.broadcastMouseDown ();
		}
		
		private function MouseEventMouseUp ( e:MouseEvent ) : void
		{
			if ( mouseOverObj ) mouseOverObj.broadcastMouseUp ();
		}
		
		//rotation around X-axis : 
		/*
		function rotateX(rot:Number,Mesh:Object3D)
		{
			var len:Number = Mesh.aVertexs.length;
			var i:Number = 0;
			while (i<len) 
			{
				var X = Mesh.aVertexs[i]["x"];
				var Y = Mesh.aVertexs[i]["y"];
				var Z = Mesh.aVertexs[i]["z"];
				var Xrotated = cos(rot)*X - sin(rot)*Z
				var Yrotated = Y
				var Zrotated = sin(rot)*X + cos(rot)*Z
				Mesh.aVertexs[i]["x"] = Xrotated;
				Mesh.aVertexs[i]["y"] = Yrotated;
				Mesh.aVertexs[i]["z"] = Zrotated;
				i++;
			}
		} 
		
		//rotation around Z-axis :
		function rotateZ(rot,Mesh:Object3D)
		{
			var len:Number = Mesh.aVertexs.length;
			var i:Number = 0;
			while (i<len) 
			{
				var X = Mesh.aVertexs[i]["x"];
				var Y = Mesh.aVertexs[i]["y"];
				var Z = Mesh.aVertexs[i]["z"];
				var Xrotated = X
				var Yrotated = cos(rot)*Y - sin(rot)*Z;
				var Zrotated = sin(rot)*Y + cos(rot)*Z;
				Mesh.aVertexs[i]["x"] = Xrotated;
				Mesh.aVertexs[i]["y"] = Yrotated;
				Mesh.aVertexs[i]["z"] = Zrotated;
				i++;
			}
		}
		
		function moveXYZ(x:Number,y:Number,z:Number,Mesh:Object3D)
		{
			var len:Number = Mesh.aVertexs.length;
			var i:Number = 0;
			for (i=0; i<len; i++)   {
				var X = Mesh.aVertexs[i]["x"];
				var Y = Mesh.aVertexs[i]["y"];
				var Z = Mesh.aVertexs[i]["z"];
				var newX = X+x;
				var newY = Y+y;
				var newZ = Z+z;
				Mesh.aVertexs[i]["x"] = newX;
				Mesh.aVertexs[i]["y"] = newY;
				Mesh.aVertexs[i]["z"] = newZ;
			}
		}

		public static function buildTrig() : void
		{
			
			var sinus=new float[4096];
			cosinus=new float[4096];
		
			for (int i=0;i<4096;i++)
			{
				sinus[i]=(float)Math.sin((float)i/rad2scale);
				cosinus[i]=(float)Math.cos((float)i/rad2scale);
			}
			trig=true;
		}*/
		
		//   ---------   RGB to HEX 
		public function rgbToHex ( r:Number, g:Number, b:Number ) : Number
		{
			return (r<<16 | g<<8 | b);
		}
		
		// ------ HEX to RGB 
		public function hexTorgb ( hex:Number ) : Object 
		{
			var red:Number = hex >> 16;
			var grnBlu:Number = hex - (red << 16);
			var grn:Number = grnBlu >> 8;
			var blu:Number = grnBlu - (grn << 8);
			return ( {r:red, g:grn, b:blu} );
		}
		
		public function toRadian ( n:Number ) : Number
		{
			return n * toRADIANS;
		}
		
	}
	
}